Skip to content

feat(spa): detail-page admin action buttons reusing the changelist actions API (#555)#562

Merged
MartinCastroAlvarez merged 1 commit into
mainfrom
feat/detail-page-actions-555
May 28, 2026
Merged

feat(spa): detail-page admin action buttons reusing the changelist actions API (#555)#562
MartinCastroAlvarez merged 1 commit into
mainfrom
feat/detail-page-actions-555

Conversation

@MartinCastroAlvarez
Copy link
Copy Markdown
Owner

Closes #555.

Why

The detail page exposed History / Edit / Delete but no admin-action buttons — so an admin's `actions = […]` was reachable from the changelist (bulk) but not from the detail of a single object. A user looking at one record had to go back to the list, single-select, and dispatch from there.

What

Why no API change

`DetailResponse` doesn't carry `actions` (the changelist actions live in the list response — and `django-admin-rest-api` owns the wire shape). DetailPage reads them via `useList({ pageSize: 1 })`; the data layer caches it. For a user who arrived from the list view, the call is already cached and essentially free.

Verification

  • Typecheck clean (13 pkgs).
  • `pnpm lint` clean — ESLint --max-warnings 0, stylelint, dark-mode guard.
  • `pnpm test` — 145 passed (unchanged; no behaviour regression).
  • `pnpm -r build` ok.

Tier 4 (frontend only, no backend / contract change).

🤖 Generated with Claude Code

…tions API (#555)

Closes #555. Surface `ModelAdmin.actions` as buttons on the detail page,
alongside `History` / `View on site` / `Edit` / `Delete`. Each button
calls the **same** changelist action endpoint the list page uses —
just with a one-pk array (`[pk]`) — so there's no new wire surface and
the existing permission gate / queryset filter / `message_user` /
intermediate-redirect-in-new-tab flow all apply unchanged.

- Imports: `useList`, `ActionDescriptor` from `@dar/data`.
- `DetailResponse` doesn't carry `actions` (they live in the list
  response — `django-admin-rest-api` owns that wire shape, no change),
  so DetailPage reads the metadata through `useList({ pageSize: 1 })`.
  The data layer caches it; for a user who arrived from the list it's
  essentially free.
- `requestDetailAction` + `performDetailAction` mirror the list-page
  flow: `requires_confirmation` opens the same styled confirm modal
  (re-reading "Run X on *this object*?"), else runs immediately;
  `result.redirect` opens in a new tab (the #250 minimum); messages
  surface as toasts (#442).
- Buttons gated by `canChange` — same visibility rule the bulk runner
  uses on the changelist.

Vitest: 145 passed. Typecheck + ESLint (--max-warnings 0) + stylelint +
dark-mode guard clean. `pnpm -r build` ok.

Closes #555

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@MartinCastroAlvarez MartinCastroAlvarez merged commit c74e252 into main May 28, 2026
5 checks passed
@MartinCastroAlvarez MartinCastroAlvarez deleted the feat/detail-page-actions-555 branch May 28, 2026 13:42
MartinCastroAlvarez pushed a commit that referenced this pull request May 28, 2026
Three SPA polish bugs reported against v1.0.1, all addressed in this PR;
no API / contract / schema change.

#570 — list filter row wrap (FilterBar trailing slot)
-----------------------------------------------------
The trailing slot rendered its children inside a sub-wrapper
"<div className=ml-auto flex flex-wrap>" — which behaved as one
flex item in the outer container. When the filter pills wrapped
onto a second line, the trailing wrapper wrapped as a unit and
ml-auto pushed it to the right edge of the new (empty) line —
producing the visually-separate "second toolbar row" the pilot
reported.

Fix: render the trailing children as DIRECT siblings of the
pills, no sub-wrapper. React.Children.toArray + cloneElement
injects ml-auto on the first non-null trailing child to push the
cluster right; flex-wrap then keeps the trailing buttons glued
to the end of the last pill row, never on a separate line.

#571 — per-object actions on detail (single-pk semantics)
---------------------------------------------------------
PR #562 (closing #555) wired the detail page to the CHANGELIST
actions endpoint with a one-pk array. That was the wrong
primitive: bulk-action verbs (selected files, selected items)
are list semantics and confused the operator on a single-object
page, and the per-object change_actions from
django-object-actions (which the API already surfaces as
data.object_actions) were visually buried.

Fix: drop the changelist-actions rendering from DetailPage
entirely. ObjectActionButton (already wired to runObjectAction
→ POST app/model/pk/action/name/) remains; it is the correct
single-pk primitive. Side effect: ~1.4 kB drop in the SPA
bundle from removing the unused useList, runAction,
confirm-modal, and runner code paths.

#572 — detail header layout (title squeezed, toolbar not right-aligned)
-----------------------------------------------------------------------
The header used sm:justify-between with no width hint on either
side, so the title block + toolbar block split horizontal space
roughly 50/50 even when the title needed more and the toolbar
would fit in less. The toolbar block had flex flex-wrap but no
justify-end, so wrapped button rows were left-aligned within the
right column — reading as "centered between title and viewport"
rather than "right-aligned to the page".

Fix: title block → min-w-0 flex-1 (claims all available width,
truncates only when it has to). Toolbar block →
shrink-0 flex-wrap justify-end. Toolbar only pushes the title
when its content genuinely needs the room, and wrapped button
rows hug the right edge.

Closes #570, #571, #572.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Detail page: per-object action buttons reusing the changelist actions API (single-pk runs)

2 participants